using System;
using System.Text;
using System.IO;

namespace OpenNETCF.Compression
{
    public abstract class GZBaseStream : Stream
    {
        protected Stream baseStream;
        internal gz_stream gzstream;
        private const int Z_BUFSIZE = 16384;
        private const int MAX_WBITS = 15; /* 32K LZ77 window */
        private const int DEF_MEM_LEVEL = 8;
        private const int EOF = -1;
        static byte[] gz_magic = new byte[] { 0x1f, 0x8b }; /* gzip magic header */

        public GZBaseStream(Stream baseStream)
        {
            this.baseStream = baseStream;
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override void Flush()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override long Length
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override long Position
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }


        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void SetLength(long value)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void Close()
        {
            baseStream.Close();
        }

        unsafe internal gz_stream GZOpen(Stream stm, CompressionLevel level, CompressionStrategy strategy, bool forReading)
        {
            ErrorCode err;
            gz_stream s = new gz_stream();

            //if (!path || !mode) return Z_NULL;


            s.stream.zalloc = IntPtr.Zero;
            s.stream.zfree = IntPtr.Zero;
            s.stream.opaque = IntPtr.Zero;
            s.stream.next_in = null;
            s.stream.next_out = null;
            s.stream.avail_in = s.stream.avail_out = 0;
            s.file = stm;
            s.z_err = ErrorCode.OK;
            s.z_eof = false;
            s.inPtr = 0;
            s.outPtr = 0;
            s.back = -1;
            s.crc = zlib_native.crc32(0, null, 0);
            s.msg = null;
            s.transparent = 0;

            s.outbuf = new byte[Z_BUFSIZE];
            s.inbuf = new byte[Z_BUFSIZE];

            unsafe
            {
                fixed (byte* pIn = &s.inbuf[0])
                fixed (byte* pOut = &s.outbuf[0])
                {
                    s.@in = pIn;
                    s.@out = pOut;

                    if (!forReading)
                    {
                        err = zlib_native.deflateInit2(ref s.stream, level,
                                           zlib_native.Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, strategy, zlib_native.zlibVersion(), sizeof(z_stream));
                        /* windowBits is passed < 0 to suppress zlib header */

                        s.stream.next_out = pOut;

                        if (err != ErrorCode.OK)
                        {
                            return null;
                        }
                    }
                    else
                    {
                        s.stream.next_in = pIn;

                        err = zlib_native.inflateInit2(ref s.stream, -MAX_WBITS, zlib_native.zlibVersion(), sizeof(z_stream));

                        /* windowBits is passed < 0 to tell that there is no zlib header.
                         * Note that in this case inflate *requires* an extra "dummy" byte
                         * after the compressed stream in order to complete decompression and
                         * return Z_STREAM_END. Here the gzip CRC32 ensures that 4 bytes are
                         * present after the compressed stream.
                         */

                        if (err != ErrorCode.OK)
                        {
                            return null;
                        }
                    }

                    s.stream.avail_out = Z_BUFSIZE;


                    if (!forReading)
                    {
                        /* Write a very simple .gz header:
                         */
                        stm.WriteByte(gz_magic[0]);
                        stm.WriteByte(gz_magic[1]);
                        stm.WriteByte(zlib_native.Z_DEFLATED);
                        stm.WriteByte(0);
                        stm.WriteByte(0);
                        stm.WriteByte(0);
                        stm.WriteByte(0);
                        stm.WriteByte(0);
                        stm.WriteByte(0);
                        stm.WriteByte(0x0b);

                        s.start = 10;
                        /* We use 10L instead of ftell(s.file) to because ftell causes an
                         * fflush on some systems. This version of the library doesn't use
                         * start anyway in write mode, so this initialization is not
                         * necessary.
                         */
                    }
                    else
                    {
                        check_header(ref s); /* skip the .gz header */
                        s.start = (int)s.file.Position - s.stream.avail_in;
                    }

                }

                return s;
            }
        }

        unsafe private bool check_header(ref gz_stream s)
        {
            int method; /* method byte */
            GZIPFlags flags;  /* flags byte */
            int len;
            int c;

            /* Assure two bytes in the buffer so we can peek ahead -- handle case
               where first byte of header is at the end of the buffer after the last
               gzip segment */
            len = s.stream.avail_in;
            if (len < 2)
            {
                if (len != 0) s.inbuf[0] = *s.stream.next_in;
                len = s.file.Read(s.inbuf, len, Z_BUFSIZE >> len);
                if (len == 0) s.z_err = ErrorCode.ErrNo;
                s.stream.avail_in += len;
                s.stream.next_in = s.@in;
                if (s.stream.avail_in < 2)
                {
                    s.transparent = s.stream.avail_in;
                    return false;
                }
            }
            unsafe
            {

                /* Peek ahead to check the gzip magic header */
                if (*(byte*)s.stream.next_in != gz_magic[0] ||
                    *((byte*)s.stream.next_in + 1) != gz_magic[1])
                {
                    s.transparent = 1;
                    return false;
                }
                s.stream.avail_in -= 2;
                s.stream.next_in += 2;

                /* Check the rest of the gzip header */
                method = get_byte(ref s);
                flags = (GZIPFlags)get_byte(ref s);
                if (method != zlib_native.Z_DEFLATED || (flags & GZIPFlags.RESERVED) != 0)
                {
                    s.z_err = ErrorCode.DataError;
                    return false;
                }

                /* Discard time, xflags and OS code: */
                for (len = 0; len < 6; len++) get_byte(ref s);

                if ((flags & GZIPFlags.EXTRA_FIELD) != 0)
                { /* skip the extra field */
                    len = get_byte(ref s);
                    len += (get_byte(ref s)) << 8;
                    /* len is garbage if EOF but the loop below will quit anyway */
                    while (len-- != 0 && get_byte(ref s) != EOF) ;
                }
                if ((flags & GZIPFlags.ORIG_NAME) != 0)
                { /* skip the original file name */
                    while ((c = get_byte(ref s)) != 0 && c != EOF) ;
                }
                if ((flags & GZIPFlags.COMMENT) != 0)
                {   /* skip the .gz file comment */
                    while ((c = get_byte(ref s)) != 0 && c != EOF) ;
                }
                if ((flags & GZIPFlags.HEAD_CRC) != 0)
                {  /* skip the header crc */
                    for (len = 0; len < 2; len++) get_byte(ref s);
                }
                s.z_err = s.z_eof ? ErrorCode.DataError : ErrorCode.OK;
            }
            return true;
        }

        unsafe private int get_byte(ref gz_stream s)
        {
            if (s.z_eof) return EOF;
            if (s.stream.avail_in == 0)
            {
                s.stream.avail_in = s.file.Read(s.inbuf, 0, Z_BUFSIZE);
                if (s.stream.avail_in == 0)
                {
                    s.z_eof = true;
                }
                s.stream.next_in = s.@in;
            }
            s.stream.avail_in--;
            byte val = *s.stream.next_in;
            s.stream.next_in++;
            return val;
        }

        private IntPtr Increment(ref IntPtr p)
        {
            return p = Increment(p);
        }

        private IntPtr Increment(IntPtr p)
        {
            unsafe
            {
                return (IntPtr)((int)p + 1);
            }
        }

        private IntPtr Increment(ref IntPtr p, int c)
        {
            return p = Increment(p, c);
        }

        private IntPtr Increment(IntPtr p, int c)
        {
            unsafe
            {
                return (IntPtr)((int)p + c);
            }
        }

        /* ===========================================================================
             Reads the given number of uncompressed bytes from the compressed file.
           gzread returns the number of bytes actually read (0 for end of file).
        */
        internal int gzread(ref gz_stream s, byte[] buf, int offset, int len)
        {
            unsafe
            {
                fixed (byte* pBuf = &buf[offset])
                fixed (byte* pInBuf = &s.inbuf[0])
                {
                    byte* start = pBuf; /* starting point for crc computation */
                    byte* next_out; /* == stream.next_out but not forced far (for MSDOS) */

                    if (s.z_err == ErrorCode.DataError || s.z_err == ErrorCode.ErrNo) return -1;
                    if (s.z_err == ErrorCode.StreamEnd) return 0;  /* EOF */

                    next_out = pBuf;
                    s.stream.next_out = pBuf;
                    s.stream.avail_out = len;

                    if (s.stream.avail_out != 0 && s.back != EOF)
                    {
                        *next_out = (byte)s.back;
                        next_out++;
                        s.stream.next_out++;
                        s.stream.avail_out--;
                        s.back = EOF;
                        s.@out++;
                        start++;
                        if (s.last != 0)
                        {
                            s.z_err = ErrorCode.StreamEnd;
                            return 1;
                        }
                    }

                    while (s.stream.avail_out != 0)
                    {

                        if (s.transparent != 0)
                        {
                            /* Copy first the lookahead bytes: */
                            int n = s.stream.avail_in;
                            if (n > s.stream.avail_out) n = s.stream.avail_out;
                            if (n > 0)
                            {
                                zlib_native.memcpy(s.stream.next_out, s.stream.next_in, n);
                                next_out += n;
                                s.stream.next_out = next_out;
                                s.stream.next_in += n;
                                s.stream.avail_out -= n;
                                s.stream.avail_in -= n;
                            }
                            if (s.stream.avail_out > 0)
                            {
                                s.stream.avail_out -=
                                    s.file.Read(buf, (int)((byte*)next_out - pBuf), s.stream.avail_out);
                            }
                            len -= s.stream.avail_out;
                            s.@in += len;
                            s.@out += len;
                            if (len == 0) s.z_eof = true;
                            return (int)len;
                        }

                        if (s.stream.avail_in == 0 && !s.z_eof)
                        {
                            s.stream.avail_in = s.file.Read(s.inbuf, 0, Z_BUFSIZE);
                            if (s.stream.avail_in == 0)
                            {
                                s.z_eof = true;
                            }
                            s.stream.next_in = pInBuf;
                        }
                        s.@in += s.stream.avail_in;
                        s.@out += s.stream.avail_out;
                        s.z_err = zlib_native.inflate(ref s.stream,  zlib_native.Z_NO_FLUSH);
                        s.@in -= s.stream.avail_in;
                        s.@out -= s.stream.avail_out;

                        if (s.z_err == ErrorCode.StreamEnd)
                        {
                            /* Check CRC and original size */
                            s.crc = zlib_native.crc32(s.crc, (IntPtr)start, (int)(s.stream.next_out - start));
                            start = s.stream.next_out;

                            if (getLong(s) != s.crc)
                            {
                                s.z_err = ErrorCode.DataError;
                            }
                            else
                            {
                                getLong(s);
                                /* The uncompressed length returned by above getlong() may be
                                 * different from s.out in case of concatenated .gz files.
                                 * Check for such files:
                                 */
                                check_header(ref s);
                                if (s.z_err == ErrorCode.OK)
                                {
                                    zlib_native.inflateReset(ref s.stream);
                                    s.crc = zlib_native.crc32(0, null, 0);
                                }
                            }
                        }
                        if (s.z_err != ErrorCode.OK || s.z_eof) break;
                    }
                    s.crc = zlib_native.crc32(s.crc, (IntPtr)start, (int)((byte*)s.stream.next_out - (byte*)start));

                    if (len == s.stream.avail_out &&
                        (s.z_err == ErrorCode.DataError || s.z_err == ErrorCode.ErrNo))
                        return -1;
                    return (int)(len - s.stream.avail_out);
                }
            }
        }

        /* ===========================================================================
             Writes the given number of uncompressed bytes into the compressed file.
           gzwrite returns the number of bytes actually written (0 in case of error).
        */
        internal int gzwrite(ref gz_stream s, byte[] buf, int offset, int len)
        {

            unsafe
            {
                fixed (byte* pBuf = &buf[offset])
                fixed (byte* pOutBuf = &s.outbuf[0])
                {

                    s.stream.next_in = pBuf;
                    s.stream.avail_in = len;

                    while (s.stream.avail_in != 0)
                    {

                        if (s.stream.avail_out == 0)
                        {

                            s.stream.next_out = pOutBuf;
                            try { s.file.Write(s.outbuf, 0, Z_BUFSIZE); }
                            catch (IOException)
                            {
                                s.z_err = ErrorCode.ErrNo;
                                break;
                            }
                            s.stream.avail_out = Z_BUFSIZE;
                        }
                        s.@in += s.stream.avail_in;
                        s.@out += s.stream.avail_out;
                        s.z_err = zlib_native.deflate(ref s.stream, zlib_native.Z_NO_FLUSH);
                        s.@in -= s.stream.avail_in;
                        s.@out -= s.stream.avail_out;
                        if (s.z_err != ErrorCode.OK) break;
                    }
                    s.crc = zlib_native.crc32(s.crc, buf, len);

                    return (int)(len - s.stream.avail_in);
                }
            }
        }

        internal ErrorCode gzflush(ref gz_stream s, int flush)
        {
            ErrorCode err = do_flush(ref s, flush);

            if (err != 0) return err;
            s.file.Flush();
            return s.z_err == ErrorCode.StreamEnd ? ErrorCode.OK : s.z_err;
        }

        private ErrorCode do_flush(ref gz_stream s, int flush)
        {
            int len;
            bool done = false;

            s.stream.avail_in = 0; /* should be zero already anyway */
            unsafe
            {
                fixed (byte* pOutBuf = &s.outbuf[0])
                {
                    while (true)
                    {
                        len = Z_BUFSIZE - s.stream.avail_out;

                        if (len != 0)
                        {
                            try
                            {
                                s.file.Write(s.outbuf, 0, len);
                            }
                            catch (IOException)
                            {
                                s.z_err = ErrorCode.ErrNo;
                                return ErrorCode.ErrNo;
                            }
                            s.stream.next_out = pOutBuf;
                            s.stream.avail_out = Z_BUFSIZE;
                        }
                        if (done) break;
                        s.@out += s.stream.avail_out;
                        s.z_err = zlib_native.deflate(ref s.stream, flush);
                        s.@out -= s.stream.avail_out;

                        /* Ignore the second of two consecutive flushes: */
                        if (len == 0 && s.z_err == ErrorCode.BufferError) s.z_err = ErrorCode.OK;

                        /* deflate has finished flushing only when it hasn't used up
                         * all the available space in the output buffer:
                         */
                        done = (s.stream.avail_out != 0 || s.z_err == ErrorCode.StreamEnd);

                        if (s.z_err != ErrorCode.OK && s.z_err != ErrorCode.StreamEnd) break;
                    }
                }
            }
            return s.z_err == ErrorCode.StreamEnd ? ErrorCode.OK : s.z_err;
        }


        private int getLong(gz_stream s)
        {
            int x = get_byte(ref s);
            int c;

            x += get_byte(ref s) << 8;
            x += get_byte(ref s) << 16;
            c = get_byte(ref s);
            if (c == EOF) s.z_err = ErrorCode.DataError;
            x += c << 24;
            return x;
        }

    }
}
